Créez un système de connexion utilisateur sécurisé avec Flask. Ce guide couvre le setup, le hachage de mots de passe, la gestion de session et les pratiques de sécurité.
Authentification Flask : Un guide complet pour construire des systèmes de connexion utilisateur sécurisés
Dans le monde numérique actuel, presque toutes les applications web significatives nécessitent un moyen de gérer et d'identifier leurs utilisateurs. Que vous construisiez un réseau social, une plateforme e-commerce ou un intranet d'entreprise, un système d'authentification sécurisé et fiable n'est pas seulement une fonctionnalité, c'est une exigence fondamentale. C'est le gardien numérique qui protège les données des utilisateurs, personnalise les expériences et instaure la confiance.
Flask, le micro-framework Python populaire, offre la flexibilité nécessaire pour construire des applications web puissantes, mais il laisse délibérément la mise en œuvre de l'authentification au développeur. Cette approche minimaliste est une force, vous permettant de choisir les meilleurs outils pour le travail sans être contraint par une méthodologie spécifique. Cependant, cela signifie également que vous êtes responsable de construire le système correctement et en toute sécurité.
Ce guide complet est conçu pour un public international de développeurs. Nous vous accompagnerons à travers chaque étape de la construction d'un système de connexion utilisateur complet et prêt pour la production avec Flask. Nous commencerons par les bases absolues et progresserons vers une solution robuste, en couvrant les pratiques de sécurité essentielles en cours de route. À la fin de ce tutoriel, vous aurez les connaissances et le code pour implémenter une inscription utilisateur sécurisée, la connexion et la gestion de session dans vos propres projets Flask.
Prérequis : Configurer votre environnement de développement
Avant d'écrire notre première ligne de code d'authentification, nous devons établir un environnement de développement propre et organisé. C'est une bonne pratique universelle en développement logiciel, garantissant que les dépendances de votre projet n'entrent pas en conflit avec d'autres projets sur votre système.
1. Python et environnements virtuels
Assurez-vous d'avoir Python 3.6 ou plus récent installé sur votre système. Nous utiliserons un environnement virtuel pour isoler les paquets de notre projet. Ouvrez votre terminal ou invite de commande et exécutez les commandes suivantes :
# Créer un répertoire de projet
mkdir flask-auth-project
cd flask-auth-project
# Créer un environnement virtuel (le dossier 'venv')
python3 -m venv venv
# Activer l'environnement virtuel
# Sur macOS/Linux:
source venv/bin/activate
# Sur Windows:
venv\\Scripts\\activate
Vous saurez que l'environnement est actif lorsque vous verrez `(venv)` précédant votre invite de commande.
2. Installation des extensions Flask essentielles
Notre système d'authentification sera construit sur un ensemble d'excellentes extensions Flask bien entretenues. Chacune a un but spécifique :
- Flask : Le framework web principal.
- Flask-SQLAlchemy : Un Mappeur Objet-Relationnel (ORM) pour interagir avec notre base de données de manière Pythonique.
- Flask-Migrate : Gère les migrations de schéma de base de données.
- Flask-WTF : Simplifie le travail avec les formulaires web, offrant une validation et une protection CSRF.
- Flask-Login : Gère la session utilisateur, s'occupant de la connexion, de la déconnexion et de la mémorisation des utilisateurs.
- Flask-Bcrypt : Fournit de solides capacités de hachage de mot de passe.
- python-dotenv : Gère les variables d'environnement pour la configuration.
Installez-les toutes avec une seule commande :
pip install Flask Flask-SQLAlchemy Flask-Migrate Flask-WTF Flask-Login Flask-Bcrypt python-dotenv
Partie 1 : La fondation - Structure du projet et modèle de base de données
Un projet bien organisé est plus facile à maintenir, à faire évoluer et à comprendre. Nous utiliserons un modèle commun de fabrique d'applications Flask.
Conception d'une structure de projet évolutive
Créez la structure de répertoires et de fichiers suivante dans votre répertoire `flask-auth-project` :
/flask-auth-project
|-- /app
| |-- /static
| |-- /templates
| | |-- base.html
| | |-- index.html
| | |-- login.html
| | |-- register.html
| | |-- dashboard.html
| |-- __init__.py
| |-- models.py
| |-- forms.py
| |-- routes.py
|-- .env
|-- config.py
|-- run.py
- /app : Le paquet principal contenant la logique de notre application.
- /templates : Contient nos fichiers HTML.
- __init__.py : Initialise notre application Flask (la fabrique d'applications).
- models.py : Définit nos tables de base de données (par exemple, le modèle User).
- forms.py : Définit nos formulaires d'inscription et de connexion à l'aide de Flask-WTF.
- routes.py : Contient nos fonctions de vue (la logique pour différentes URL).
- config.py : Stocke les paramètres de configuration de l'application.
- run.py : Le script principal pour démarrer le serveur web.
- .env : Un fichier pour stocker les variables d'environnement comme les clés secrètes (ce fichier ne DOIT PAS être committé au contrôle de version).
Configuration de votre application Flask
Remplissons nos fichiers de configuration.
Fichier .env :
Créez ce fichier à la racine de votre projet. C'est là que nous stockerons les informations sensibles.
SECRET_KEY='une-cle-secrete-tres-forte-et-longue-aleatoire'
DATABASE_URL='sqlite:///site.db'
IMPORTANT : Remplacez la valeur de `SECRET_KEY` par votre propre chaîne longue, aléatoire et imprévisible. Cette clé est cruciale pour sécuriser les sessions utilisateur.
Fichier config.py :
Ce fichier lit la configuration de notre fichier `.env`.
import os
from dotenv import load_dotenv
basedir = os.path.abspath(os.path.dirname(__file__))
load_dotenv(os.path.join(basedir, '.env'))
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY')
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \\
'sqlite:///' + os.path.join(basedir, 'app.db')
SQLALCHEMY_TRACK_MODIFICATIONS = False
Création du modèle utilisateur avec Flask-SQLAlchemy
Le modèle User est le cœur de notre système d'authentification. Il définit la structure de la table `users` dans notre base de données.
app/models.py :
from flask_login import UserMixin
from . import db, login_manager
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(128), nullable=False)
def __repr__(self):
return f'<User {self.username}>'
Décomposons cela :
- `UserMixin` : Il s'agit d'une classe de `Flask-Login` qui inclut des implémentations génériques pour des méthodes comme `is_authenticated`, `is_active`, etc., dont notre modèle User a besoin.
- `@login_manager.user_loader` : Cette fonction est une exigence pour `Flask-Login`. Elle est utilisée pour recharger l'objet utilisateur à partir de l'ID utilisateur stocké dans la session. Flask-Login appellera cette fonction à chaque requête pour un utilisateur connecté.
- `password_hash` : Remarquez que nous ne stockons PAS le mot de passe directement. Nous stockons un `password_hash`. C'est l'un des principes de sécurité les plus critiques en matière d'authentification. Stocker des mots de passe en clair est une vulnérabilité de sécurité majeure. Si votre base de données est un jour compromise, les attaquants auront accès aux mots de passe de tous les utilisateurs. En stockant un hachage, vous rendez le processus de récupération des mots de passe originaux pratiquement infaisable.
Initialisation de l'application
Maintenant, lions tout cela dans notre fabrique d'applications.
app/__init__.py :
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_bcrypt import Bcrypt
from flask_login import LoginManager
from config import Config
db = SQLAlchemy()
bcrypt = Bcrypt()
login_manager = LoginManager()
login_manager.login_view = 'main.login' # Page de redirection pour les utilisateurs non connectés
login_manager.login_message_category = 'info' # Classe Bootstrap pour les messages
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)
db.init_app(app)
bcrypt.init_app(app)
login_manager.init_app(app)
from .routes import main as main_blueprint
app.register_blueprint(main_blueprint)
return app
run.py :
from app import create_app, db
from app.models import User
app = create_app()
@app.shell_context_processor
def make_shell_context():
return {'db': db, 'User': User}
if __name__ == '__main__':
app.run(debug=True)
Avant de pouvoir exécuter l'application, nous devons créer la base de données. Depuis votre environnement virtuel activé dans le terminal, exécutez ces commandes :
flask shell
>>> from app import db
>>> db.create_all()
>>> exit()
Cela créera un fichier `site.db` dans votre répertoire racine, contenant la table `user` que nous avons définie.
Partie 2 : Construction de la logique d'authentification principale
Une fois les fondations en place, nous pouvons maintenant construire les parties visibles par l'utilisateur : les formulaires d'inscription et de connexion ainsi que les routes qui les traitent.
Inscription de l'utilisateur : Enregistrer de nouveaux utilisateurs en toute sécurité
D'abord, nous définons le formulaire en utilisant Flask-WTF.
app/forms.py :
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, BooleanField
from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError
from .models import User
class RegistrationForm(FlaskForm):
username = StringField('Nom d'utilisateur',
validators=[DataRequired(), Length(min=2, max=20)])
email = StringField('Email',
validators=[DataRequired(), Email()])
password = PasswordField('Mot de passe', validators=[DataRequired()])
confirm_password = PasswordField('Confirmer le mot de passe',
validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('S'inscrire')
def validate_username(self, username):
user = User.query.filter_by(username=username.data).first()
if user:
raise ValidationError('Ce nom d'utilisateur est déjà pris. Veuillez en choisir un autre.')
def validate_email(self, email):
user = User.query.filter_by(email=email.data).first()
if user:
raise ValidationError('Cet email est déjà enregistré. Veuillez en choisir un autre.')
class LoginForm(FlaskForm):
email = StringField('Email',
validators=[DataRequired(), Email()])
password = PasswordField('Mot de passe', validators=[DataRequired()])
remember = BooleanField('Se souvenir de moi')
submit = SubmitField('Se connecter')
Remarquez les validateurs personnalisés `validate_username` et `validate_email`. Flask-WTF appelle automatiquement toute méthode suivant le modèle `validate_<field_name>` et l'utilise comme validateur personnalisé pour ce champ. C'est ainsi que nous vérifions si un nom d'utilisateur ou un email est déjà dans la base de données.
Ensuite, nous créons la route pour gérer l'inscription.
app/routes.py :
from flask import Blueprint, render_template, url_for, flash, redirect, request
from .forms import RegistrationForm, LoginForm
from .models import User
from . import db, bcrypt
from flask_login import login_user, current_user, logout_user, login_required
main = Blueprint('main', __name__)
@main.route('/')
@main.route('/index')
def index():
return render_template('index.html')
@main.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = RegistrationForm()
if form.validate_on_submit():
hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
user = User(username=form.username.data, email=form.email.data, password_hash=hashed_password)
db.session.add(user)
db.session.commit()
flash('Votre compte a été créé ! Vous pouvez maintenant vous connecter', 'success')
return redirect(url_for('main.login'))
return render_template('register.html', title='S'inscrire', form=form)
Hachage de mot de passe avec Flask-Bcrypt
La ligne la plus importante du code ci-dessus est :
hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
Bcrypt est un algorithme de hachage moderne et adaptatif. Il prend le mot de passe de l'utilisateur et effectue une transformation unidirectionnelle complexe et coûteuse en calcul. Il intègre également un "sel" aléatoire pour chaque mot de passe afin de prévenir les attaques par table arc-en-ciel. Cela signifie que même si deux utilisateurs ont le même mot de passe, leurs hachages stockés seront complètement différents. Le hachage résultant est ce que nous stockons dans la base de données. Il est pratiquement impossible d'inverser ce processus pour obtenir le mot de passe original.
Connexion de l'utilisateur : Authentifier les utilisateurs existants
Maintenant, ajoutons la route de connexion Ă notre fichier `app/routes.py`.
app/routes.py (ajouter cette route) :
@main.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user and bcrypt.check_password_hash(user.password_hash, form.password.data):
login_user(user, remember=form.remember.data)
next_page = request.args.get('next')
return redirect(next_page) if next_page else redirect(url_for('main.index'))
else:
flash('Connexion échouée. Veuillez vérifier votre email et mot de passe', 'danger')
return render_template('login.html', title='Connexion', form=form)
Les étapes clés ici sont :
- Trouver l'utilisateur : Nous interrogeons la base de données pour un utilisateur avec l'adresse e-mail soumise.
- Vérifier le mot de passe : C'est la vérification cruciale : `bcrypt.check_password_hash(user.password_hash, form.password.data)`. Cette fonction prend le hachage stocké de notre base de données et le mot de passe en texte clair que l'utilisateur vient d'entrer. Elle re-hache le mot de passe soumis en utilisant le même sel (qui est stocké dans le hachage lui-même) et compare les résultats. Elle renvoie `True` seulement s'ils correspondent. Cela nous permet de vérifier un mot de passe sans jamais avoir besoin de déchiffrer le hachage stocké.
- Gérer la session : Si le mot de passe est correct, nous appelons `login_user(user, remember=form.remember.data)`. Cette fonction de `Flask-Login` enregistre l'utilisateur comme connecté, stockant son ID dans la session utilisateur (un cookie sécurisé côté serveur). L'argument `remember` gère la fonctionnalité "Se souvenir de moi".
Déconnexion de l'utilisateur : Terminer une session en toute sécurité
La déconnexion est simple. Nous avons juste besoin d'une route qui appelle la fonction `logout_user` de `Flask-Login`.
app/routes.py (ajouter cette route) :
@main.route('/logout')
def logout():
logout_user()
return redirect(url_for('main.index'))
Cette fonction effacera l'ID de l'utilisateur de la session, le déconnectant ainsi.
Partie 3 : Protéger les routes et gérer les sessions utilisateur
Maintenant que les utilisateurs peuvent se connecter et se déconnecter, nous devons utiliser leur état authentifié.
Protéger le contenu avec `@login_required`
De nombreuses pages, comme le tableau de bord d'un utilisateur ou les paramètres de compte, ne devraient être accessibles qu'aux utilisateurs connectés. `Flask-Login` rend cela incroyablement simple avec le décorateur `@login_required`.
Créons une route de tableau de bord protégée.
app/routes.py (ajouter cette route) :
@main.route('/dashboard')
@login_required
def dashboard():
return render_template('dashboard.html', title='Tableau de bord')
C'est tout ! Si un utilisateur non connecté tente de visiter `/dashboard`, `Flask-Login` interceptera automatiquement la requête et le redirigera vers la page de connexion (que nous avons configurée dans `app/__init__.py` avec `login_manager.login_view = 'main.login'`). Après une connexion réussie, il le redirigera intelligemment vers la page du tableau de bord à laquelle il tentait d'accéder à l'origine.
Accéder aux informations de l'utilisateur actuel
Dans vos routes et templates, `Flask-Login` fournit un objet proxy magique appelé `current_user`. Cet objet représente l'utilisateur actuellement connecté pour la requête active. Si aucun utilisateur n'est connecté, il s'agit d'un objet utilisateur anonyme où `current_user.is_authenticated` sera `False`.
Vous pouvez l'utiliser dans votre code Python :
# Dans une route
if current_user.is_authenticated:
print(f'Bonjour, {current_user.username}!')
Et vous pouvez également l'utiliser directement dans vos templates Jinja2 :
<!-- Dans un template comme base.html -->
{% if current_user.is_authenticated %}
<a href="{{ url_for('main.dashboard') }}">Tableau de bord</a>
<a href="{{ url_for('main.logout') }}">Déconnexion</a>
{% else %}
<a href="{{ url_for('main.login') }}">Connexion</a>
<a href="{{ url_for('main.register') }}">Inscription</a>
{% endif %}
Cela vous permet de modifier dynamiquement la barre de navigation ou d'autres parties de votre interface utilisateur en fonction de l'état de connexion de l'utilisateur.
Templates HTML
Pour être complet, voici quelques templates de base que vous pouvez placer dans le répertoire `app/templates`. Ils utilisent du HTML simple mais peuvent être facilement intégrés avec un framework comme Bootstrap ou Tailwind CSS.
base.html :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>{{ title }} - Application d'authentification Flask</title>
</head>
<body>
<nav>
<a href="{{ url_for('main.index') }}">Accueil</a>
{% if current_user.is_authenticated %}
<a href="{{ url_for('main.dashboard') }}">Tableau de bord</a>
<a href="{{ url_for('main.logout') }}">Déconnexion</a>
{% else %}
<a href="{{ url_for('main.login') }}">Connexion</a>
<a href="{{ url_for('main.register') }}">Inscription</a>
{% endif %}
</nav>
<hr>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</body>
</html>
register.html / login.html (exemple utilisant le formulaire d'inscription) :
{% extends "base.html" %}
{% block content %}
<div>
<form method="POST" action="">
{{ form.hidden_tag() }}
<fieldset>
<legend>Rejoignez-nous aujourd'hui</legend>
<div>
{{ form.username.label }}
{{ form.username() }}
</div>
<div>
{{ form.email.label }}
{{ form.email() }}
</div>
<div>
{{ form.password.label }}
{{ form.password() }}
</div>
<div>
{{ form.confirm_password.label }}
{{ form.confirm_password() }}
</div>
</fieldset>
<div>
{{ form.submit() }}
</div>
</form>
</div>
{% endblock content %}
Partie 4 : Sujets avancés et meilleures pratiques de sécurité
Le système que nous avons construit est solide, mais une application de niveau production exige davantage. Voici les prochaines étapes essentielles et les considérations de sécurité.
1. Fonctionnalité de réinitialisation de mot de passe
Les utilisateurs oublieront inévitablement leurs mots de passe. Un flux de réinitialisation de mot de passe sécurisé est crucial. Le processus standard et sécurisé est le suivant :
- L'utilisateur saisit son adresse e-mail sur une page "Mot de passe oublié".
- L'application génère un jeton sécurisé, à usage unique et sensible au temps. La bibliothèque `itsdangerous` (installée avec Flask) est parfaite pour cela.
- L'application envoie un e-mail Ă l'utilisateur contenant un lien avec ce jeton.
- Lorsque l'utilisateur clique sur le lien, l'application valide le jeton (vérifiant sa validité et son expiration).
- Si valide, l'utilisateur est présenté avec un formulaire pour saisir et confirmer un nouveau mot de passe.
N'envoyez jamais l'ancien mot de passe d'un utilisateur ou un nouveau mot de passe en texte clair par e-mail.
2. Confirmation par e-mail lors de l'inscription
Pour empêcher les utilisateurs de s'inscrire avec de fausses adresses e-mail et pour vous assurer que vous pouvez les contacter, vous devriez implémenter une étape de confirmation par e-mail. Le processus est très similaire à une réinitialisation de mot de passe : générer un jeton, envoyer un lien de confirmation par e-mail, et avoir une route qui valide le jeton et marque le compte de l'utilisateur comme `confirmed` dans la base de données.
3. Limitation du taux pour prévenir les attaques par force brute
Une attaque par force brute se produit lorsqu'un attaquant essaie à plusieurs reprises différents mots de passe sur un formulaire de connexion. Pour atténuer cela, vous devriez implémenter une limitation du taux. Cela restreint le nombre de tentatives de connexion qu'une seule adresse IP peut effectuer dans un certain laps de temps (par exemple, 5 tentatives échouées par minute). L'extension `Flask-Limiter` est un excellent outil pour cela.
4. Utilisation des variables d'environnement pour tous les secrets
Nous l'avons déjà fait pour nos `SECRET_KEY` et `DATABASE_URL`, ce qui est excellent. C'est une pratique critique pour la sécurité et la portabilité. Ne commettez jamais votre fichier `.env` ou tout fichier contenant des identifiants codés en dur (comme les clés API ou les mots de passe de base de données) dans un système de contrôle de version public comme GitHub. Utilisez toujours un fichier `.gitignore` pour les exclure.
5. Protection contre les attaques Cross-Site Request Forgery (CSRF)
Bonne nouvelle ! En utilisant `Flask-WTF` et en incluant `{{ form.hidden_tag() }}` dans nos formulaires, nous avons déjà activé la protection CSRF. Cette balise cachée génère un jeton unique pour chaque soumission de formulaire, garantissant que la requête provient de votre site réel et non d'une source externe malveillante essayant de tromper vos utilisateurs.
Conclusion : Vos prochaines étapes en authentification Flask
Félicitations ! Vous avez réussi à construire un système d'authentification utilisateur complet et sécurisé avec Flask. Nous avons couvert l'intégralité du cycle de vie : la mise en place d'un projet évolutif, la création d'un modèle de base de données, la gestion sécurisée de l'inscription utilisateur avec le hachage de mot de passe, l'authentification des utilisateurs, la gestion des sessions avec Flask-Login et la protection des routes.
Vous disposez maintenant d'une base robuste que vous pouvez intégrer en toute confiance dans n'importe quel projet Flask. N'oubliez pas que la sécurité est un processus continu, et non une configuration unique. Les principes que nous avons abordés, en particulier le hachage des mots de passe et la protection des clés secrètes, sont non négociables pour toute application qui gère des données utilisateur.
À partir de là , vous pouvez explorer des sujets d'authentification encore plus avancés pour améliorer davantage votre application :
- Contrôle d'accès basé sur les rôles (RBAC) : Ajoutez un champ `role` à votre modèle utilisateur pour accorder des permissions différentes aux utilisateurs réguliers et aux administrateurs.
- Intégration OAuth : Permettez aux utilisateurs de se connecter en utilisant des services tiers comme Google, GitHub ou Facebook.
- Authentification à deux facteurs (2FA) : Ajoutez une couche de sécurité supplémentaire en exigeant un code d'une application d'authentification ou par SMS.
En maîtrisant les fondamentaux de l'authentification, vous avez fait un pas significatif dans votre parcours de développeur web professionnel. Bon codage !